#!/usr/bin/python
# -*- coding : utf-8 -*-
# Create Time : 2006-1-17 12:09:56

#===============================================================================
# L port forward main file
#===============================================================================
#TODO: Client Acl

from twisted.application import service, internet
from twisted.internet import protocol, reactor, defer, task
from twisted.protocols import irc, portforward
import datetime
import logging
import os
import re
import string
import sys
import time

#===============================================================================
# config script path in here
#===============================================================================
_path = 'D:\\eclipse\\workspace\\LogPortForward'
sys.path.append( _path )
try :
    import lpfconfig
except ImportError:
    print 'You MUST Config lpf.py file, change _path to local path'
    sys.exit()

#===============================================================================
# OK , try to import lpf config file
#===============================================================================

global root_path
global server_log_file
global allow_database
global ACL

# try load pickle module
try:
    import cPickle as pickle
except ImportError:
    import pickle
    
# try to load inetwork module, if module import false, ACL Module will set disable    
try:
    import inetwork
    ACL = True
except ImportError:
    ACL = False
    print 'Import Module Error, Access Control Module Disable'    

#===============================================================================
# registration custom Error exception
#===============================================================================
class IrcMsgQueueIsEmptyError( Exception ):
    ''' registration when irc msg queue is empty
    '''
    
    def __init__( self ):
        pass
    
    def __str__( self ):
        return 'Irc_Msg_Queue_Is_Empty'
    
class KickLinkQueueIsEmptyError( Exception ):
    '''registration when kick queue is empty
    '''
    
    def __init__( self ):
        pass
    
    def __str__( self ):
        return 'Kick_Link_Queue_Is_Empty'
    
class IrcBot( irc.IRCClient ):
    '''Irc protocol 
        Assumpsit : All irc msg put to irc message queue
    '''
    
    global irc_log 
    global pf_log
    global main_log
    global manager_log
    global ACL

    def __init__( self ):
        self.nickname = lpfconfig.irc_nick
        self.password = lpfconfig.irc_server_pw
        self.re_command = re.compile( '^\^' )

    def connectionMade( self ):
        irc_log.info( 'Connect to irc server' ) #IGNORE:E0602
        
        # define a task , read irc_msg_queue, post it to channel
        self.post2channel = task.LoopingCall( self.do_post )
        self.post2channel.start( 0.05 )
        
        irc.IRCClient.connectionMade( self )

    def connectionLost( self, reason ):
        irc_log.info( 'Connect lost, will reconnect it' )#IGNORE:E0602
        
        # stop task
        self.post2channel.stop()
        #TODO: reconnect to irc server
        irc.IRCClient.connectionLost( self, reason )

    def do_post( self ):
        '''read from irc_msg_queue, post it to channel
        '''
        try:
            self._talk2channel( lpfconfig.irc_channel, self.factory.get_irc_msg_queue() ) #IGNORE:E0201
        except IrcMsgQueueIsEmptyError:
            pass
        
    def signedOn( self ):
        """Called when bot has succesfully signed on to server."""
        irc_log.info( 'Signed into irc' )#IGNORE:E0602
        self.msg( 'NickServ', ' IDENTIFY %s' % lpfconfig.irc_nick_pw )
        self.join( lpfconfig.irc_channel, lpfconfig.irc_channel_pw )

    def joined( self, channel ):
        irc_log.info( 'Joined channell' )#IGNORE:E0602

    def privmsg( self, user, channel, msg ):
        """This will get called when the bot receives a message."""
        user = user.split( '!', 1 )[0]
        
        i_msg = msg.strip()
        
        #check command from channel
        if channel != lpfconfig.irc_channel:
            return 
        
        #check is command ?
        if not self.re_command.search( i_msg ) :
            #not a irc command , skip it and return 
            return
        else:
            # is a irc command . continue running
            pass
        
        i_tmp_msg  = self.re_command.split( i_msg )[1].split()
        
        i_command  = i_tmp_msg[0]
        i_command_parm  = i_tmp_msg[1:]
        
        if hasattr( self, 'do_'+i_command ):
            getattr( self, 'do_'+i_command )( i_command_parm )

    def do_help( self, *args ):
        help_msg = '''-----------------------------------------------------
^help                Help
^show_link           Show current connections client info
^show_acl            Show Current connect ACL
^add_acl 0.0.0.0     Add ACL 192.168.0.0/16 or 192.168.1.1/32 etc.......
^del_acl 0.0.0.0     Delete Acl 192.168.0.0/16 or 192.168.1.1/32 etc.......
^kick link_no        Kick current connect by link number
-----------------------------------------------------'''

        #put msg to irc msg queue
        self.factory.put_irc_msg_queue( help_msg ) #IGNORE:E0201
        
    def do_info( self ):
        pass
        
    def do_show_link( self, *args ):
        _msg = self.factory.get_user_session() #IGNORE:E0201
        if _msg != '':
            self.factory.put_irc_msg_queue( _msg ) #IGNORE:E0201
        else:
            self.factory.put_irc_msg_queue( 'not connection' ) #IGNORE:E0201
            
    def do_show_acl( self, *args ):
        global ACL
        if ACL == False:
            _msg = 'Import Module Error, Access Control Module Disable'
            self.factory.put_irc_msg_queue( _msg ) #IGNORE:E0201
            return
                
    def do_kick( self, links ):
        
        _tmp_link_id = []
        
        # check input parm has link_id ?
        for x in links:
            if self.factory.has_user_session( x ): #IGNORE:E0201
                _tmp_link_id.append( x )
            else:
                self.factory.put_irc_msg_queue( 'No such Link_id %s' % x ) #IGNORE:E0201
        
        # check finish , put link_id to kick_queue
        self.factory.put_linkid_to_kick_queue( _tmp_link_id ) #IGNORE:E0201
    
    def do_add_acl( self, *args ):
        global ACL
        if ACL == False:
            _msg = 'Import Module Error, Access Control Module Disable'
            self.factory.put_irc_msg_queue( _msg ) #IGNORE:E0201
            return
    
    def do_del_acl( self, *args ):
        global ACL
        if ACL == False:
            _msg = 'Import Module Error, Access Control Module Disable'
            self.factory.put_irc_msg_queue( _msg ) #IGNORE:E0201
            return
    
    def action( self, user, channel, msg ):
        """This will get called when the bot sees someone do an action."""
        user = user.split( '!', 1 )[0]
        
    def modeChanged( self, user, channel, set, modes, args ):
        pass
        
    def kickedFrom( self, channel, kicker, message ):
        self.signedOn()
        self.say( channel, 'Why me??' )
        
    def userJoined( self, user, channel ):
        self.mode( channel, set='+', modes='o', limit=None, user=user, mask=None )
        
    def irc_NICK( self, prefix, params ):
        """Called when an IRC user changes their nickname."""
        old_nick = prefix.split( '!' )[0]
        new_nick = params[0]
    
    def _talk2user( self, iu , m ):
        for x in m.split( '\n' ):
            self.msg( iu, x )
            
    def _talk2channel( self, ic, m ):
        for x in m.split( '\n' ):
            self.say( ic, x )
        

class LoggingProxyClient( portforward.ProxyClient ):
    '''Server to remote Protocol
    '''
        
    def dataReceived( self, data ):
        portforward.ProxyClient.dataReceived( self, data )

class LoggingProxyServer( portforward.ProxyServer ):
    '''Client to Server protocol
    '''

    global irc_log 
    global pf_log
    global main_log
    global manager_log
    
    def connectionMade( self ):
        
        # get connect client infomation 
        file_no = self.transport.fileno()
        session_no = self.transport.sessionno
        client_from_ip  = self.transport.getPeer().host
        client_from_port  = self.transport.getPeer().port
        
        # set user session
        self.factory.update_user_session( file_no, session_no, client_from_ip, client_from_port ) #IGNORE:E0201
        
        # put irc msg queue
        self.factory.put_irc_msg_queue( \
                                       '[%s] - Client Connect from %s_%s [%s:%s]' % \
                                       ( time.ctime(), \
                                        str( file_no ), \
                                        str( session_no ), \
                                        client_from_ip, \
                                        client_from_port ) ) #IGNORE:E0201
        
        pf_log.info( '[%s] - Client Connect from %s_%s [%s:%s]' % \
                                       ( time.ctime(), \
                                        str( file_no ), \
                                        str( session_no ), \
                                        client_from_ip, \
                                        client_from_port ) )
                                        
        #TODO: check client in allow list ?
        portforward.ProxyServer.connectionMade( self )
        
    def dataReceived( self, data ):
        
        file_no = self.transport.fileno()
        session_no = self.transport.sessionno
        client_from_ip  = self.transport.getPeer().host
        client_from_port  = self.transport.getPeer().port
        
        link_id = '%s_%s' % ( str( file_no ), str( session_no ) )
        if self.factory.should_linkid_need_kick( link_id ):
            pf_log.info( '[%s] - Client was Kicked from %s_%s [%s:%s]' % \
                                       ( time.ctime(), \
                                        str( file_no ), \
                                        str( session_no ), \
                                        client_from_ip, \
                                        client_from_port ) )
            #Drop Client Connections
            self.transport.loseConnection()
            
            # put irc msg queue
            self.factory.put_irc_msg_queue( \
                                            '[%s] - Client was Kicked from %s_%s [%s:%s]' % \
                                       ( time.ctime(), \
                                        str( file_no ), \
                                        str( session_no ), \
                                        client_from_ip, \
                                        client_from_port ) ) 
            #IGNORE:E0201
                                        
            #TODO: When success close client connections , should del link_id from kick queue
            
            return 
        portforward.ProxyServer.dataReceived( self, data )

    def connectionLost( self, reason ):
        
        # get connect client infomation 
        file_no = self.transport.fileno()
        session_no = self.transport.sessionno
        client_from_ip  = self.transport.getPeer().host
        client_from_port  = self.transport.getPeer().port

        # put irc msg queue
        self.factory.put_irc_msg_queue( '[%s] - Client Disconnect from %s_%s [%s:%s] %s' % \
                                       ( time.ctime(), \
                                        str( file_no ), \
                                        str( session_no ), \
                                        client_from_ip, \
                                        client_from_port, \
                                        reason.getErrorMessage() ) ) #IGNORE:E0201
        
        #Logging Client Connect lost
        pf_log.info( '[%s] - Client Disconnect from %s_%s [%s:%s] %s' % \
                    ( time.ctime(), \
                     str( file_no ), \
                     str( session_no ), \
                     client_from_ip, \
                     client_from_port, \
                     reason.getErrorMessage() ) )
        
        #remove link session
        self.factory.remove_user_session( session_no ) #IGNORE:E0201
        
class LPFServices( service.Service ):  
    '''Global Services , keep global var, return Multi Factory
    '''
    
    def __init__( self ):
        
        global root_path
        #get program path
        root_path = os.getcwd()
    
        #Set running env
        set_env( root_path )
        
        global server_log_file
        logging.basicConfig( level=logging.DEBUG, \
                    format='%(asctime)s %(name)-12s %(levelname)-8s %(message)s', \
                    filename=server_log_file, \
                    filemode='w' )#IGNORE:E0602
                    
        global irc_log 
        global pf_log
        global main_log
        global manager_log
        irc_log = logging.getLogger( 'IRC_LOG' )
        pf_log = logging.getLogger( 'PF_LOG' )
        main_log = logging.getLogger( 'SERVER_LOG' )            
        manage_log = logging.getLogger( 'MANAGE_LOG' )
    
        main_log.info( 'L Port Forward Server start' )
        
        # when start service , read acl file
        self.allow_list = self._read_acl()
        
        #Set a task , auto save acl to backup file
        auto_save = task.LoopingCall( self._save_acl )
        auto_save.start( 60*5 ) # 5 min
        
        # {'file_no - sess_no':[login_time,connect_from_ip,connect_from_port]}
        # TAG: u_session should be fileno
        self.user_session = {}
    
        #Irc msg queue
        self.irc_msg_queue = []
        
        #Client kick queue
        # queue item is link_id
        self.kick_queue = []
        
        #compile regx
        self.re_session = re.compile( '^[0-9]*_' )
        self.re_fileno = re.compile( '_[0-9]*$' )
        
#===============================================================================
#        # Set a task , kick link_no from kick_queue
#        auto_kick = task.LoopingCall( self.do_kick_link )
#        auto_kick.start(1)
#===============================================================================
        
    def update_user_session( self, f_no, sess_no, s_ip, s_port ):
        '''when user connect to server , update user session
            used by proxy server protocol
        @param s_ip:
        @param s_port:
        '''
        
        self.user_session[str( f_no )+'_'+str( sess_no )] = [time.time(), s_ip, s_port]
        
        
    def get_user_session( self ):
        '''when got irc command , return user_session
            used by irc protocol
        '''
        
        t = []
        for session_id, time_host_port in self.user_session.iteritems():
            
            #check link uptime
            start_time_obj = self._get_datetime_obj( int( time_host_port[0] ) )
            current_time_obj = self._get_datetime_obj()
            up_time = str( current_time_obj - start_time_obj )
            
            t.append( 'Link: %s|From: %s:%s |Uptime: %s\n' % \
                     ( string.rjust( str( session_id ), 7 ), time_host_port[1], time_host_port[2], up_time ) )
        return ''.join( t )
    
    def has_user_session( self, link_no ):
        '''check link_no in user_session list ?
            used by irc protocol
        
        @param link_no:
        '''
        
        if self.user_session.has_key( link_no ):
            return True
        else:
            return False
        
    def remove_user_session( self, sess_no ):
        '''when user diconnect , remove user session
            used by proxy protocol
        '''
        
        tmp = {}
        re_session = re.compile( '^[0-9]*_' )
        for link_no in self.user_session.keys():
            session_id = self._get_session_no( link_no )
            tmp[session_id] = link_no
            
        if str( sess_no ) in tmp.keys():
            del self.user_session[tmp[str( sess_no )]]
        
        del tmp
    
    def _get_datetime_obj( self, dita=None ):
        if dita == None:
            dita = time.time()
        _t = time.localtime( dita )
        ye = _t[0]
        mo = _t[1]
        da = _t[2]
        ho = _t[3]
        mi = _t[4]
        se = _t[5]
        try:
            return datetime.datetime( ye, mo, da, ho, mi, se )    
        except TypeError:
            return datetime.datetime( 2006, 1, 1, 0, 00, 00 )    

    def _get_file_no( self, l_no ):
        '''input link_id ,return file like object
        
        @param l_i:
        '''
        return self.re_fileno.split( l_no )[0]
    
    def _get_session_no( self, l_no ):
        '''input link_id , return session_no
        
        @param l_i:
        '''
        return self.re_session.split( l_no )[1]
        
    def put_linkid_to_kick_queue( self, linkno ):
        '''when user want to kick link, then put link_id in kick_queue
            used by irc client protocol
        @param linkid:
        '''
        self.kick_queue += linkno
        
    def remove_linkid_from_kick_queue( self, l_i ):
        '''When success close link , must del link_if from kick_queue
            used by Client to Server protocol
        @param l_i:
        '''
        
        pass
        
    def should_linkid_need_kick( self, l_i ):
        '''check linkid shoule be kick ?
            used by Proxy Client protocol
        
        @param l_i:
        '''
        if l_i in self.kick_queue:
            return True
        else:
            return False
        
    def _read_acl( self ):
        '''read acl
        '''
        global main_log
        global allow_database
        try:
            fp = open( allow_database, 'r' )
        except IOError:
            self.allow_list = ['0.0.0.0']
        main_log.info( 'read acl file' )
        
    def _save_acl( self ):
        '''save  acl 
        '''
        
        global main_log
        main_log.info( 'save acl file' )
    
    def isValidClient( self, address ):
        '''check ipaddress in allow lists
    
            @param address:
        '''
    
        #TODO: if address in deny list ,return 'inDeny', if not in allow list return 'notAllow'
        pass
    
    
    def showAllow( self, address ):
        pass
    
    def removeAllow( self, address ):
        pass
    
    def addDeny( self, address ):
        pass
    
    def removeDeny( self, address ):
        pass  
    
    def put_irc_msg_queue( self, m ):  
        '''append irc msg queue
            used by irc protocol an proxy protcol
        '''
        self.irc_msg_queue.append( m )
    
    def get_irc_msg_queue( self ):
        '''read all queue from irc_msg_queue
            used by irc protocol
        '''
        try:
            return self.irc_msg_queue.pop( 0 ) 
        except IndexError:
            raise IrcMsgQueueIsEmptyError
    

    def getLoggingProxyFactory( self, host, port ):
        '''Port Forward Server Factory method, handle all client to server connections
        '''
        
        server_f = portforward.ProxyFactory( host, port )
        server_f.protocol = LoggingProxyServer
        
        # reg user session protocol
        server_f.update_user_session = self.update_user_session
        server_f.remove_user_session = self.remove_user_session
        
        # reg irc msg queue protocol
        server_f.put_irc_msg_queue = self.put_irc_msg_queue
        
        # reg should kick link id protocol
        server_f.should_linkid_need_kick = self.should_linkid_need_kick
        
        return server_f
        
    def getLoggingProxyClientFactory( self ):
        '''pORT fORWARD cLIENT fACTORY METHOD, handle server to remote connections
        '''
        
        client_f = portforward.ProxyClientFactory()
        client_f.protocol = LoggingProxyClient
        return client_f
    
    def getIrcBotFactory( self ):
        '''make a irc client, user can do some manage via command line
        '''
        
        irc_f = protocol.ReconnectingClientFactory()
        irc_f.protocol = IrcBot
        
        # reg user session protocol
        irc_f.get_user_session = self.get_user_session
        irc_f.has_user_session = self.has_user_session
        
        # reg irc msg queue protocol
        irc_f.get_irc_msg_queue = self.get_irc_msg_queue
        irc_f.put_irc_msg_queue = self.put_irc_msg_queue
        
        #reg for irc, kick link queue protocol
        irc_f.put_linkid_to_kick_queue = self.put_linkid_to_kick_queue
        
        return irc_f
    
#===============================================================================
# Utils       
#===============================================================================

def set_env( r_path ):
    '''Set program running env , make log file path 
    
    @param root_path:
    '''
    
    global server_log_file
    global allow_database
    
    server_log_file = os.path.join( r_path, 'lpf_server.log' )
    allow_database = os.path.join( r_path, 'allow.db' )
    
#===============================================================================
# Start all Services
#===============================================================================
application = service.Application( 'LPORTFORWARD' )
f = LPFServices()
serviceCollection = service.IServiceCollection( application )
#print 'Setup Server.............'

#Connect to Irc server
#Check use ssl connect ?    
ircfactory = f.getIrcBotFactory()

if lpfconfig.irc_server_link_type == 'SSL':
    from twisted.internet import ssl
        
    irc_ssl_connect = internet.SSLClient( lpfconfig.irc_server_ip, \
                           int( lpfconfig.irc_server_port ), \
                           ircfactory, \
                           ssl.ClientContextFactory() ).setServiceParent( serviceCollection )
        
else:
    irc_connect = internet.TCPClient( lpfconfig.irc_server_ip, \
                           int( lpfconfig.irc_server_port ), \
                           ircfactory ).setServiceParent( serviceCollection )
        
#start Port forward server

pffactory = f.getLoggingProxyFactory( lpfconfig.pf_remote_ipaddress, \
                                         int( lpfconfig.pf_remote_port ) )
        
pf_listen = internet.TCPServer( int( lpfconfig.pf_local_port ), \
                    pffactory ).setServiceParent( serviceCollection )
